/* -*- coding: utf-8; tab-width: 4 -*-
vim: ts=4 noet ai */

/**
	Desktopd Rainbow Persona -- Colorful Firefox

	Copyright © 2016  Desktopd Project

	This program is free software: you can redistribute it and/or modify
	it under the terms of the GNU General Public License as
	published by the Free Software Foundation, either version 3 of the
	License, or (at your option) any later version.

	This program is distributed in the hope that it will be useful,
	but WITHOUT ANY WARRANTY; without even the implied warranty of
	MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
	GNU General Public License for more details.

	You should have received a copy of the GNU General Public License
	along with this program.  If not, see <http://www.gnu.org/licenses/>.

	@license GPL-3.0+
	@file
*/

'use strict';

const {setTimeout} = require ('sdk/timers');
const {Hotkey} = require ('sdk/hotkeys');
const simplePrefs = require ('sdk/simple-prefs');
const {prefs} = require ('sdk/simple-prefs');
const {changeRGB, getCurrent} = require ('./display');

const CIELUV = require ('./colors/CIELUV');
const CIELAB = require ('./colors/CIELAB');
const YCbCr = require ('./colors/YCbCr');

const BOUNDARY_REFLECT = 1;
const BOUNDARY_RESET = 2;

const TRANSITION_NEWTONIAN = 1;
const TRANSITION_UNIFORM = 2;
const TRANSITION_EXPONENTIAL = 3;
const TRANSITION_EXPONENTIAL_DIFF = 4;

const COLORSPACE_CIELUV = 1;
const COLORSPACE_CIELAB = 2;
const COLORSPACE_YCBCR = 3;
const COLORSPACE_RGB = 0;

const WHITEPOINT_A = 2856;
const WHITEPOINT_D50 = 5003;
const WHITEPOINT_D55 = 5503;
const WHITEPOINT_D65 = 6504;
const WHITEPOINT_D75 = 7504;

// Typical range
const SCALE_CIELUV = 150;
const SCALE_CIELAB = 100;
const SCALE_YCBCR = 120;
const SCALE_RGB = 0.5;

// x, y
const A = [0.44757, 0.40745];
const D50 = [0.34567, 0.35850];
const D55 = [0.33242, 0.34743];
const D65 = [0.31271, 0.32902];
const D75 = [0.29902, 0.31485];

const toXYZ = (x, y, Y) => [Y / y * x, Y, Y / y * (1 - x - y)];


const getWhitepoint = () => {
	if (WHITEPOINT_A === prefs.whitepoint) {
		return toXYZ (... A, 1);
	}
	if (WHITEPOINT_D50 === prefs.whitepoint) {
		return toXYZ (... D50, 1);
	}
	if (WHITEPOINT_D55 === prefs.whitepoint) {
		return toXYZ (... D55, 1);
	}
	if (WHITEPOINT_D65 === prefs.whitepoint) {
		return toXYZ (... D65, 1);
	}
	if (WHITEPOINT_D75 === prefs.whitepoint) {
		return toXYZ (... D75, 1);
	}
	
	throw new Error ('Unknown whitepoint');
};

const randomUniform = (min, max) => min + Math.floor (Math.random () * (max - min));
const inRangeRGB = (r, g, b) => r >= 0 && r <= 1 && g >= 0 && g <= 1 && b >= 0 && b <= 1;
const randomExponential = mean => - Math.log (1 - Math.random ()) * mean;
const randomExponential2 = mean => (Math.round (Math.random ()) ? 1 : -1)
	* randomExponential (mean);

// Simplest
const updateRandomUniform = () => {
	const whitepoint = getWhitepoint ();
	if (COLORSPACE_CIELUV === prefs.colorspace) {
		const maxValues = [];
		const minValues = [];
		[[0, 0, 0], [0, 0, 1], [0, 1, 0], [0, 1, 1], [1, 0, 0], [1, 0, 1], [1, 1, 0], [1, 1, 1]]
		.forEach (rgb => {
			const [L, U, V] = CIELUV.fromRGB (... whitepoint, ... rgb);
			maxValues[0] = null == maxValues[0] ? L : Math.max (maxValues[0], L);
			maxValues[1] = null == maxValues[1] ? U : Math.max (maxValues[1], U);
			maxValues[2] = null == maxValues[2] ? V : Math.max (maxValues[2], V);
			minValues[0] = null == minValues[0] ? L : Math.min (minValues[0], L);
			minValues[1] = null == minValues[1] ? U : Math.min (minValues[1], U);
			minValues[2] = null == minValues[2] ? V : Math.min (minValues[2], V);
		});
		
		let r, g, b;
		do {
			const L = randomUniform (minValues[0], maxValues[0]);
			const U = randomUniform (minValues[1], maxValues[1]);
			const V = randomUniform (minValues[2], maxValues[2]);
			[r, g, b] = CIELUV.toRGB (... whitepoint, L, U, V);
		} while (!inRangeRGB (r, g, b));
		
		changeRGB (r, g, b);
	} else if (COLORSPACE_CIELAB === prefs.colorspace) {
		const maxValues = [];
		const minValues = [];
		[[0, 0, 0], [0, 0, 1], [0, 1, 0], [0, 1, 1], [1, 0, 0], [1, 0, 1], [1, 1, 0], [1, 1, 1]]
		.forEach (rgb => {
			const [L, A, B] = CIELAB.fromRGB (... whitepoint, ... rgb);
			maxValues[0] = null == maxValues[0] ? L : Math.max (maxValues[0], L);
			maxValues[1] = null == maxValues[1] ? A : Math.max (maxValues[1], A);
			maxValues[2] = null == maxValues[2] ? B : Math.max (maxValues[2], B);
			minValues[0] = null == minValues[0] ? L : Math.min (minValues[0], L);
			minValues[1] = null == minValues[1] ? A : Math.min (minValues[1], A);
			minValues[2] = null == minValues[2] ? B : Math.min (minValues[2], B);
		});
		
		let r, g, b;
		do {
			const L = randomUniform (minValues[0], maxValues[0]);
			const A = randomUniform (minValues[1], maxValues[1]);
			const B = randomUniform (minValues[2], maxValues[2]);
			[r, g, b] = CIELAB.toRGB (... whitepoint, L, A, B);
		} while (!inRangeRGB (r, g, b));
		
		changeRGB (r, g, b);
	} else if (COLORSPACE_YCBCR === prefs.colorspace) {
		const maxValues = [];
		const minValues = [];
		[[0, 0, 0], [0, 0, 1], [0, 1, 0], [0, 1, 1], [1, 0, 0], [1, 0, 1], [1, 1, 0], [1, 1, 1]]
		.forEach (rgb => {
			const [Y, Cb, Cr] = YCbCr.fromRGB (... rgb);
			maxValues[0] = null == maxValues[0] ? Y : Math.max (maxValues[0], Y);
			maxValues[1] = null == maxValues[1] ? Cb : Math.max (maxValues[1], Cb);
			maxValues[2] = null == maxValues[2] ? Cr : Math.max (maxValues[2], Cr);
			minValues[0] = null == minValues[0] ? Y : Math.min (minValues[0], Y);
			minValues[1] = null == minValues[1] ? Cb : Math.min (minValues[1], Cb);
			minValues[2] = null == minValues[2] ? Cr : Math.min (minValues[2], Cr);
		});
		
		let r, g, b;
		do {
			const Y = randomUniform (minValues[0], maxValues[0]);
			const Cb = randomUniform (minValues[1], maxValues[1]);
			const Cr = randomUniform (minValues[2], maxValues[2]);
			[r, g, b] = YCbCr.toRGB (Y, Cb, Cr);
		} while (!inRangeRGB (r, g, b));
		
		changeRGB (r, g, b);
	} else {
		// RGB
		changeRGB (Math.random (), Math.random (), Math.random ());
	}
};

// RGB
const getOriginColor = () => {
	const hexColor = prefs['origin'];
	return [hexColor.substr (1, 2), hexColor.substr (3, 2), hexColor.substr (5, 2)]
	.map (str => parseInt (str, 0x10) / 255);
};

const updateRandomExponential = () => {
	const whitepoint = getWhitepoint ();
	let origin = getOriginColor ();
	let scale;
	if (COLORSPACE_CIELUV === prefs.colorspace) {
		origin = CIELUV.fromRGB (... whitepoint, ... origin);
		scale = SCALE_CIELUV;
	} else if (COLORSPACE_CIELAB === prefs.colorspace) {
		origin = CIELAB.fromRGB (... whitepoint, ... origin);
		scale = SCALE_CIELAB;
	} else if (COLORSPACE_YCBCR === prefs.colorspace) {
		origin = YCbCr.fromRGB (... origin);
		scale = SCALE_YCBCR;
	}
	
	const mean = prefs.scale / 1000 * scale;
	let rgb;
	do {
		if (rgb && BOUNDARY_RESET === prefs['transition.boundaryMode']) {
			updateRandomUniform ();
			return;
		}
		
		const point = origin.map (t => t + randomExponential2 (mean));
		
		if (COLORSPACE_CIELUV === prefs.colorspace) {
			rgb = CIELUV.toRGB (... whitepoint, ... point);
		} else if (COLORSPACE_CIELAB === prefs.colorspace) {
			rgb = CIELAB.toRGB (... whitepoint, ... point);
		} else if (COLORSPACE_YCBCR === prefs.colorspace) {
			rgb = YCbCr.toRGB (... point);
		}
	} while (!inRangeRGB (... rgb));
	
	changeRGB (... rgb);
};

const updateRandomExponentialDiff = () => {
	const whitepoint = getWhitepoint ();
	let origin = getCurrent ();
	let scale;
	if (COLORSPACE_CIELUV === prefs.colorspace) {
		origin = CIELUV.fromRGB (... whitepoint, ... origin);
		scale = SCALE_CIELUV;
	} else if (COLORSPACE_CIELAB === prefs.colorspace) {
		origin = CIELAB.fromRGB (... whitepoint, ... origin);
		scale = SCALE_CIELAB;
	} else if (COLORSPACE_YCBCR === prefs.colorspace) {
		origin = YCbCr.fromRGB (... origin);
		scale = SCALE_YCBCR;
	}
	
	const mean = prefs.scale / 1000 * scale;
	let rgb;
	do {
		if (rgb && BOUNDARY_RESET === prefs['transition.boundaryMode']) {
			updateRandomUniform ();
			return;
		}
		
		const point = origin.map (t => t + randomExponential2 (mean));
		
		if (COLORSPACE_CIELUV === prefs.colorspace) {
			rgb = CIELUV.toRGB (... whitepoint, ... point);
		} else if (COLORSPACE_CIELAB === prefs.colorspace) {
			rgb = CIELAB.toRGB (... whitepoint, ... point);
		} else if (COLORSPACE_YCBCR === prefs.colorspace) {
			rgb = YCbCr.toRGB (... point);
		}
	} while (!inRangeRGB (... rgb));
	
	changeRGB (... rgb);
};

let velocity = [0, 0, 0];
const updateNewtonian = () => {
	const whitepoint = getWhitepoint ();
	let origin = getCurrent () || getOriginColor ();
	let scale;
	if (COLORSPACE_CIELUV === prefs.colorspace) {
		origin = CIELUV.fromRGB (... whitepoint, ... origin);
		scale = SCALE_CIELUV;
	} else if (COLORSPACE_CIELAB === prefs.colorspace) {
		origin = CIELAB.fromRGB (... whitepoint, ... origin);
		scale = SCALE_CIELAB;
	} else if (COLORSPACE_YCBCR === prefs.colorspace) {
		origin = YCbCr.fromRGB (... origin);
		scale = SCALE_YCBCR;
	}
	
	const mean = prefs.scale / 1000 * scale;
	let rgb;
	do {
		if (rgb && BOUNDARY_RESET === prefs['transition.boundaryMode']) {
			updateRandomUniform ();
			velocity = [0, 0, 0];
			return;
		}
		
		velocity = velocity.map (t =>
			t * Math.min (1, Math.max (0, 1 - prefs.resistance / 1000))
			+ randomExponential2 (mean));
		
		const point = origin.map ((t, i) => t + velocity[i]);
		
		if (COLORSPACE_CIELUV === prefs.colorspace) {
			rgb = CIELUV.toRGB (... whitepoint, ... point);
		} else if (COLORSPACE_CIELAB === prefs.colorspace) {
			rgb = CIELAB.toRGB (... whitepoint, ... point);
		} else if (COLORSPACE_YCBCR === prefs.colorspace) {
			rgb = YCbCr.toRGB (... point);
		}
	} while (!inRangeRGB (... rgb));
	
	changeRGB (... rgb);
};


const update = once => {
	if (TRANSITION_UNIFORM === prefs['transition.type']) {
		updateRandomUniform ();
	} if (TRANSITION_EXPONENTIAL === prefs['transition.type']) {
		updateRandomExponential ();
	} if (TRANSITION_EXPONENTIAL_DIFF === prefs['transition.type']) {
		updateRandomExponentialDiff ();
	} else {
		updateNewtonian ();
	}
	
	once || setTimeout (update, prefs.interval * 1000);
};

update ();

let hotkey;
const hotkeyListener = () => {
	hotkey && hotkey.destroy ();
	if (prefs.enableHotkey) {
		hotkey = Hotkey ({combo: prefs.hotkey, onPress () {
			update (true);
		}});
	}
};
simplePrefs.on ('enableHotkey', hotkeyListener);
simplePrefs.on ('hotkey', hotkeyListener);
hotkeyListener ();

